from typing import Any, Dict, Optional, Tuple
from pathlib import Path
import os
import pandas as pd
import pickle
import numpy as np
import json

import torch
from lightning import LightningDataModule
from torch.utils.data import DataLoader, Dataset


class DynamicLanguageModelDataset(Dataset):
    def __init__(self, data):
        self.data = data

    def __len__(self):
        return len(self.data)

    def __getitem__(self, idx):
        return self.data[idx]
    

class ProteinGymDataset(Dataset):
    def __init__(self, data_dir):
        self.data_dir = data_dir
        self.data_names = os.listdir(self.data_dir)

    def __len__(self):
        return len(self.data_names)

    def __getitem__(self, idx):
        return self.data_names[idx]


class DynamicLanguageModelDataModule(LightningDataModule):

    def __init__(
        self,
        data_dir: str = "data/",
        batch_size: int = 16,
        num_workers: int = 0,
        pin_memory: bool = False,
    ) -> None:
        """Initialize a `MNISTDataModule`.

        :param data_dir: The data directory. Defaults to `"data/"`.
        :param train_val_test_split: The train, validation and test split. Defaults to `(55_000, 5_000, 10_000)`.
        :param batch_size: The batch size. Defaults to `64`.
        :param num_workers: The number of workers. Defaults to `0`.
        :param pin_memory: Whether to pin memory. Defaults to `False`.
        """
        super().__init__()

        # this line allows to access init params with 'self.hparams' attribute
        # also ensures init params will be stored in ckpt
        self.save_hyperparameters(logger=False)
        self.data_train: Optional[Dataset] = None
        self.data_val: Optional[Dataset] = None
        self.data_test: Optional[Dataset] = None

    @property
    def num_classes(self) -> int:  
        return 1

    def prepare_data(self) -> None:
        """Download data if needed. Lightning ensures that `self.prepare_data()` is called only
        within a single process on CPU, so you can safely add your downloading logic within. In
        case of multi-node training, the execution of this hook depends upon
        `self.prepare_data_per_node()`.

        Do not use it to assign state (self.x = y).
        """
        pass

    def setup(self, stage: Optional[str] = None) -> None:
        """Load data. Set variables: `self.data_train`, `self.data_val`, `self.data_test`.

        This method is called by Lightning before `trainer.fit()`, `trainer.validate()`, `trainer.test()`, and
        `trainer.predict()`, so be careful not to execute things like random split twice! Also, it is called after
        `self.prepare_data()` and there is a barrier in between which ensures that all the processes proceed to
        `self.setup()` once the data is prepared and available for use.

        :param stage: The stage to setup. Either `"fit"`, `"validate"`, `"test"`, or `"predict"`. Defaults to ``None``.
        """
        self.hparams.data_dir = Path(self.hparams.data_dir)
        self.hparams.test_dir = self.hparams.data_dir/'proteingym'
        self.hparams.train_dir = self.hparams.data_dir/'dynamic_language_model'

        # load and split datasets only if not loaded already
        if not self.data_test or not self.data_train or not self.data_val:
            self.train_data_path = "/home/zhangqiang/userdata/wangzeyuan/Dynamic/Dynamic.Quantizer/output/data.jsonl"
            with open(self.train_data_path, "r") as f_in:
                self.data = [json.loads(line) for line in f_in]
            self.data = [item for item in self.data if len(item['sequence']) < 512]
            self.data_train = DynamicLanguageModelDataset(self.data[:int(len(self.data))])
            # self.data_val = DynamicLanguageModelDataset(self.data[int(len(self.data)*0.95):])
            self.data_val = ProteinGymDataset(self.hparams.test_dir)
            self.data_test = ProteinGymDataset(self.hparams.test_dir)

    def train_collator(self, raw_batch):
        return raw_batch
    
    def test_collator(self, raw_batch):
        data_item = raw_batch[0]
        data_name, _ = data_item.split()
        data_path = self.hparams.test_dir / data_item / (data_name + ".json")
        mutant_path = self.hparams.test_dir / data_item / (data_name + ".csv")

        return data_item, data_path, mutant_path

    def train_dataloader(self) -> DataLoader[Any]:
        return DataLoader(
            dataset=self.data_train,
            batch_size=self.hparams.batch_size,
            num_workers=self.hparams.num_workers,
            pin_memory=self.hparams.pin_memory,
            collate_fn=self.train_collator,
            shuffle=True,
        )
    
    def val_dataloader(self) -> DataLoader[Any]:
        return DataLoader(
            dataset=self.data_val,
            batch_size=self.hparams.batch_size,
            num_workers=self.hparams.num_workers,
            pin_memory=self.hparams.pin_memory,
            collate_fn=self.test_collator,
            shuffle=False,
        )
    
    def test_dataloader(self) -> DataLoader[Any]:
        return DataLoader(
            dataset=self.data_test,
            batch_size=self.hparams.batch_size,
            num_workers=self.hparams.num_workers,
            pin_memory=self.hparams.pin_memory,
            collate_fn=self.test_collator,
            shuffle=False,
        )

    def teardown(self, stage: Optional[str] = None) -> None:
        """Lightning hook for cleaning up after `trainer.fit()`, `trainer.validate()`,
        `trainer.test()`, and `trainer.predict()`.

        :param stage: The stage being torn down. Either `"fit"`, `"validate"`, `"test"`, or `"predict"`.
            Defaults to ``None``.
        """
        pass

    def state_dict(self) -> Dict[Any, Any]:
        """Called when saving a checkpoint. Implement to generate and save the datamodule state.

        :return: A dictionary containing the datamodule state that you want to save.
        """
        return {}

    def load_state_dict(self, state_dict: Dict[str, Any]) -> None:
        """Called when loading a checkpoint. Implement to reload datamodule state given datamodule
        `state_dict()`.

        :param state_dict: The datamodule state returned by `self.state_dict()`.
        """
        pass


if __name__ == "__main__":
    pass
